package pl.agh.pp.zephyr.algorithms;

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Queue;

import pl.agh.pp.zephyr.algorithms.Command.ArgKey;
import pl.agh.pp.zephyr.integration.GkController;
import pl.agh.pp.zephyr.logic.Engine;
import pl.agh.pp.zephyr.model.Mcu;
import pl.agh.pp.zephyr.model.Room;
import pl.agh.pp.zephyr.utils.LogPublisher;

/**
 * Właściwe wykonanie routowania - wykorzystując algorytm routowania
 * <code>algorithm</code> realizuje listę rozkazów, które ten zapoda.
 * 
 * @author czopson
 * 
 */
public class CallRouter extends LogPublisher implements Runnable {

	private RoutingAlgorithm algorithm;

	private final Engine engine;

	public CallRouter(final Engine engine) {
		super(CallRouter.class);
		this.engine = engine;
		try {
			new Thread(this).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	protected Queue<RouteRequest> q = new LinkedList<RouteRequest>();
	
	/**
	 * Realizacja rozkazu przeroutowania <code>routeRequest</code>.
	 * 
	 * pewnie synchronizacja tej metody nie jest potrzebna - należy przemyśleć,
	 * czy coś może się np rozspójnić, jeśli nie będzie to synchronizowane
	 * 
	 * być może wystarczy synchronizować jedynie dostęp do modelu? TODO
	 * 
	 */
	public synchronized void routeCall(final RouteRequest request) {
		q.offer(request);
	}
	
	private synchronized void service() {
		final RouteRequest request = q.poll();
		if (request == null) {
			return;
		}
		Command com = null;
		for (ListIterator<Command> iter = algorithm.calculateRoute(request,
				engine.area).listIterator(); iter.hasNext();) {
			try {
				com = iter.next();
				// próba wykonania
				performCommand(com, request.getCaller().gk.controller);
			} catch (Exception e) {
				// jeśli coś się nie udało to próbujemy to naprawić
				log.warning("Failed to perform command: " + com);
				e.printStackTrace();
				log.info("Trying to compensate...");
				// wycofywanie operacji w odwrotnej kolejności
				while (iter.hasPrevious()) {
					com = iter.previous();
					log.info("Compensation of " + com + ": "
							+ (compensate(com) ? "OK" : "FAILED"));
				}
				// a na końcu i tak trzeba się poddać
				return;
			}
		}
	}

	/**
	 * Próba cofnięcia oferacji <code>previous</code>
	 * 
	 * @param previous
	 * @return czy kompensacja się udała
	 */
	private boolean compensate(Command previous) {
		// TODO
		return false;
	}

	/**
	 * Właściwie wykonanie polecenia <code>com</code>
	 * 
	 * @param com
	 * @throws Exception
	 */
	private void performCommand(Command com, GkController gk) throws Exception {
		switch (com.type) {
		// stworzenie pokoju
		case CREATE_ROOM: {
			final Mcu m = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.CREATE_ROOM_MCU_HASH), Mcu.class);
			final String alias = (String) com.params
					.get(Command.ArgKey.CREATE_ROOM_ROOM_ALIAS);
			if (engine.area.createRoom(alias, m.hashCode()) == null) {
				throw new Exception("Failed to create new room");
			}
			// m.controller.createRoom(alias);
			break;
		}
			// migracja użytkownika, czyli wykopanie go z jednego pokoju i
			// podłącznie do drugiego
			// TODO może być problem z czasem trwania tej operacji (za długa) i
			// synchronizacją metody routeCall
		case MIGRATE_USER: {
			// TODO
			break;
		}
			// poprostu przekierowanie na istniejący pokój
		case ROUTE_SIMPLE: {
			gk.routeToAlias((String) com.params.get(ArgKey.ROUTE_SIMPLE_ALIAS),
					(String) com.params
							.get(ArgKey.ROUTE_SIMPLE_CALLING_ENDPOINT_ID),
					(String) com.params.get(ArgKey.ROUTE_SIMPLE_CALL_REF));
			break;
		}

		case ROUTE_TO_GATEWAY: {
			gk.routeToGateway((String) com.params
					.get(ArgKey.ROUTE_TO_GATEWAY_ALIAS), (String) com.params
					.get(ArgKey.ROUTE_TO_GATEWAY_IP), (String) com.params
					.get(ArgKey.ROUTE_TO_GATEWAY_PORT), (String) com.params
					.get(ArgKey.ROUTE_TO_GATEWAY_CALLING_EP_ID),
					(String) com.params.get(ArgKey.ROUTE_TO_GATEWAY_CALL_REF));
			break;
		}
			// połączenie pokoji
		case LINK_ROOMS: {
			final Room from = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.LINK_ROOMS_ROOM_FROM_HASH), Room.class);
			final Room to = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.LINK_ROOMS_ROOM_FROM_HASH), Room.class);
			if (from == null || to == null) {
				throw new Exception("Failed to find rooms to be linked");
			}
			from.mcu.controller.connectRooms(from.alias, to.alias, to.mcu.ip,
					to.mcu.port);
			// TODO sprawdzić czy się udało
			from.connections.add(to);
			to.connections.add(from);
			break;
		}
		case CONNECT_ROOM_TO_MCU: {
			final Room room = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.CONNECT_ROOM_TO_MCU_ROOM_HASH),
					Room.class);
			final Mcu mcu = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.CONNECT_ROOM_TO_MCU_MCU_HASH),
					Mcu.class);
			if (!room.mcus.contains(mcu)) {
				room.mcus.add(mcu);
			}
			if (!mcu.rooms.contains(room)) {
				mcu.rooms.add(room);
			}
			break;
		}

		case ROUTE_REJECT: {
			gk.routeReject((String) com.params
					.get(Command.ArgKey.ROUTE_REJECT_CALLING_EP_ID),
					(String) com.params
							.get(Command.ArgKey.ROUTE_REJECT_CALL_REF));
			break;
		}

		case CONNECT_ROOMS: {
			final Mcu from = engine.area
					.getElement((Integer) com.params
							.get(Command.ArgKey.CONNECT_ROOMS_MCU_FROM_HASH),
							Mcu.class);
			final Mcu to = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.CONNECT_ROOMS_MCU_TO_HASH), Mcu.class);
			final Room room = engine.area.getElement((Integer) com.params
					.get(Command.ArgKey.CONNECT_ROOMS_ROOM_HASH), Room.class);
			room.mcus.add(from);
			room.mcus.add(to);
			from.rooms.add(room);
			to.rooms.add(room);
			from.controller
					.connectRooms(room.alias, room.alias, to.ip, to.port);
			break;
		}

		}
	}

	public RoutingAlgorithm getAlgorithm() {
		return algorithm;
	}

	public synchronized void setAlgorithm(RoutingAlgorithm algorithm) {
		this.algorithm = algorithm;
	}

	@Override
	public void run() {
		while(true) {
			while (q.isEmpty()) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			service();
		}
	}

}
